//go:build ee

/*
                  Kubermatic Enterprise Read-Only License
                         Version 1.0 ("KERO-1.0”)
                     Copyright © 2025 Kubermatic GmbH

   1.	You may only view, read and display for studying purposes the source
      code of the software licensed under this license, and, to the extent
      explicitly provided under this license, the binary code.
   2.	Any use of the software which exceeds the foregoing right, including,
      without limitation, its execution, compilation, copying, modification
      and distribution, is expressly prohibited.
   3.	THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
      MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
      IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
      CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
      TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
      SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

   END OF TERMS AND CONDITIONS
*/

package policybindingcontroller

import (
	"context"
	"encoding/json"
	"fmt"
	"reflect"

	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
	"go.uber.org/zap"

	kubermaticv1 "k8c.io/kubermatic/sdk/v2/apis/kubermatic/v1"
	userclustercontrollermanager "k8c.io/kubermatic/v2/pkg/controller/user-cluster-controller-manager"
	kuberneteshelper "k8c.io/kubermatic/v2/pkg/kubernetes"
	kkpreconciling "k8c.io/kubermatic/v2/pkg/resources/reconciling"
	"k8c.io/reconciler/pkg/reconciling"

	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/controller-runtime/pkg/builder"
	ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/manager"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
	"sigs.k8s.io/controller-runtime/pkg/source"
)

const (
	ControllerName   = "kkp-policy-binding-controller"
	cleanupFinalizer = "kubermatic.k8c.io/cleanup-policy-binding"

	// Labels for Kyverno resources generated by this controller.
	LabelPolicyBinding  = "kubermatic.k8c.io/policy-binding"
	LabelPolicyTemplate = "kubermatic.k8c.io/policy-template"

	// Annotations for Kyverno resources.
	AnnotationTitle       = "policies.kyverno.io/title"
	AnnotationDescription = "policies.kyverno.io/description"
	AnnotationCategory    = "policies.kyverno.io/category"
	AnnotationSeverity    = "policies.kyverno.io/severity"
)

type reconciler struct {
	seedClient ctrlruntimeclient.Client
	userClient ctrlruntimeclient.Client

	log             *zap.SugaredLogger
	recorder        record.EventRecorder
	namespace       string
	clusterName     string
	clusterIsPaused userclustercontrollermanager.IsPausedChecker
}

// Add creates the controller and registers watches.
func Add(seedMgr, userMgr manager.Manager, log *zap.SugaredLogger, namespace, clusterName string, clusterIsPaused userclustercontrollermanager.IsPausedChecker) error {
	r := &reconciler{
		seedClient:      seedMgr.GetClient(),
		userClient:      userMgr.GetClient(),
		log:             log.Named(ControllerName),
		recorder:        userMgr.GetEventRecorderFor(ControllerName),
		namespace:       namespace,
		clusterName:     clusterName,
		clusterIsPaused: clusterIsPaused,
	}

	builderCtrl := builder.ControllerManagedBy(userMgr).
		Named(ControllerName).
		WithOptions(controller.Options{MaxConcurrentReconciles: 1}).
		WatchesRawSource(source.Kind(seedMgr.GetCache(), &kubermaticv1.PolicyBinding{},
			handler.TypedEnqueueRequestsFromMapFunc(mapPolicyBindingToRequest(namespace)),
			predicate.Or(
				predicate.TypedGenerationChangedPredicate[*kubermaticv1.PolicyBinding]{},
				predicate.TypedAnnotationChangedPredicate[*kubermaticv1.PolicyBinding]{},
			),
		)).
		WatchesRawSource(source.Kind(seedMgr.GetCache(), &kubermaticv1.PolicyTemplate{},
			handler.TypedEnqueueRequestsFromMapFunc(mapPolicyTemplateToRequest(r.seedClient, namespace, r.log)),
		)).
		WatchesRawSource(source.Kind(userMgr.GetCache(), &kyvernov1.ClusterPolicy{},
			handler.TypedEnqueueRequestsFromMapFunc(mapClusterPolicyToRequest(namespace)),
		)).
		WatchesRawSource(source.Kind(userMgr.GetCache(), &kyvernov1.Policy{},
			handler.TypedEnqueueRequestsFromMapFunc(mapPolicyToRequest(namespace)),
		))

	_, err := builderCtrl.Build(r)
	return err
}

// Reconcile reconciles a single PolicyBinding.
func (r *reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
	log := r.log.With("binding", req.NamespacedName)
	log.Debug("Reconciling")

	paused, err := r.clusterIsPaused(ctx)
	if err != nil {
		return reconcile.Result{}, fmt.Errorf("failed to check cluster pause status: %w", err)
	}
	if paused {
		return reconcile.Result{}, nil
	}

	binding := &kubermaticv1.PolicyBinding{}
	if err := r.seedClient.Get(ctx, req.NamespacedName, binding); err != nil {
		log.Debug("policy binding not found, namespace: %s, name: %s", req.Namespace, req.Name)
		return reconcile.Result{}, ctrlruntimeclient.IgnoreNotFound(err)
	}

	cluster := &kubermaticv1.Cluster{}
	if err := r.seedClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: r.clusterName}, cluster); err != nil {
		log.Debug("cluster not found, name: %s", r.clusterName)
		return reconcile.Result{}, err
	}

	if err := r.reconcile(ctx, log, binding, cluster); err != nil {
		r.recorder.Event(binding, corev1.EventTypeWarning, "ReconcilingError", err.Error())
		return reconcile.Result{}, err
	}

	return reconcile.Result{}, nil
}

func (r *reconciler) reconcile(ctx context.Context, log *zap.SugaredLogger, binding *kubermaticv1.PolicyBinding, cluster *kubermaticv1.Cluster) error {
	// Keep a copy of the original binding for status patching.
	oldBinding := binding.DeepCopy()
	defer func() {
		if statusErr := r.updateStatus(ctx, oldBinding, binding); statusErr != nil {
			log.Errorw("Failed to update status", "error", statusErr)
		}
	}()

	// Handle cleanup when Kyverno is disabled or cluster is being deleted.
	if !cluster.Spec.IsKyvernoEnabled() || !cluster.DeletionTimestamp.IsZero() {
		if kuberneteshelper.HasFinalizer(binding, cleanupFinalizer) {
			return r.handlePolicyBindingCleanup(ctx, binding)
		}
		return nil
	}

	if !binding.DeletionTimestamp.IsZero() {
		return r.handlePolicyBindingCleanup(ctx, binding)
	}

	if err := kuberneteshelper.TryAddFinalizer(ctx, r.seedClient, binding, cleanupFinalizer); err != nil {
		return fmt.Errorf("failed to add finalizer: %w", err)
	}

	template := &kubermaticv1.PolicyTemplate{}
	if err := r.seedClient.Get(ctx, ctrlruntimeclient.ObjectKey{Name: binding.Spec.PolicyTemplateRef.Name}, template); err != nil {
		if apierrors.IsNotFound(err) {
			binding.SetCondition(kubermaticv1.PolicyBindingConditionTemplateValid, metav1.ConditionFalse, kubermaticv1.PolicyBindingReasonTemplateNotFound, fmt.Sprintf("PolicyTemplate %s not found", binding.Spec.PolicyTemplateRef.Name))
			binding.SetCondition(kubermaticv1.PolicyBindingConditionReady, metav1.ConditionFalse, kubermaticv1.PolicyBindingReasonTemplateNotFound, "Referenced PolicyTemplate does not exist")
			binding.SetStatusFields(nil, false)
			return r.handlePolicyBindingCleanup(ctx, binding)
		}
		return err
	}

	if template.DeletionTimestamp != nil {
		binding.SetCondition(kubermaticv1.PolicyBindingConditionTemplateValid, metav1.ConditionFalse, kubermaticv1.PolicyBindingReasonTemplateNotFound, "Referenced PolicyTemplate is being deleted")
		binding.SetCondition(kubermaticv1.PolicyBindingConditionReady, metav1.ConditionFalse, kubermaticv1.PolicyBindingReasonTemplateNotFound, "Referenced PolicyTemplate is being deleted")
		binding.SetStatusFields(template, false)
		return r.handlePolicyBindingCleanup(ctx, binding)
	}

	binding.SetCondition(kubermaticv1.PolicyBindingConditionTemplateValid, metav1.ConditionTrue, kubermaticv1.PolicyBindingReasonPolicyApplied, "Referenced PolicyTemplate is valid")

	var reconcileErr error
	if template.Spec.NamespacedPolicy {
		reconcileErr = r.reconcileNamespacedPolicy(ctx, log, template, binding)
	} else {
		reconcileErr = r.reconcileClusterPolicy(ctx, log, template, binding)
	}

	if reconcileErr != nil {
		binding.SetCondition(kubermaticv1.PolicyBindingConditionKyvernoPolicyApplied, metav1.ConditionFalse, kubermaticv1.PolicyBindingReasonApplyFailed, reconcileErr.Error())
		binding.SetCondition(kubermaticv1.PolicyBindingConditionReady, metav1.ConditionFalse, kubermaticv1.PolicyBindingReasonApplyFailed, "Failed to apply Kyverno Policy")
		binding.SetStatusFields(template, false)
		return reconcileErr
	}

	binding.SetCondition(kubermaticv1.PolicyBindingConditionKyvernoPolicyApplied, metav1.ConditionTrue, kubermaticv1.PolicyBindingReasonPolicyApplied, "Kyverno Policy successfully created/updated")
	binding.SetCondition(kubermaticv1.PolicyBindingConditionReady, metav1.ConditionTrue, kubermaticv1.PolicyBindingReasonReady, "PolicyBinding is ready")
	binding.SetStatusFields(template, true)

	return nil
}

// reconcileNamespacedPolicy reconciles a namespaced Kyverno Policy.
func (r *reconciler) reconcileNamespacedPolicy(ctx context.Context, log *zap.SugaredLogger, template *kubermaticv1.PolicyTemplate, binding *kubermaticv1.PolicyBinding) error {
	targetNamespace := binding.Spec.KyvernoPolicyNamespace.Name

	nsFactory := []reconciling.NamedNamespaceReconcilerFactory{
		r.namespaceReconcilerFactory(log, targetNamespace),
	}
	if err := reconciling.ReconcileNamespaces(ctx, nsFactory, "", r.userClient); err != nil {
		return fmt.Errorf("failed to reconcile namespace %s: %w", targetNamespace, err)
	}

	policyFactories := []kkpreconciling.NamedKyvernoPolicyReconcilerFactory{
		r.kyvernoPolicyFactory(template, binding),
	}

	if err := kkpreconciling.ReconcileKyvernoPolicys(ctx, policyFactories, targetNamespace, r.userClient); err != nil {
		return fmt.Errorf("failed to reconcile Kyverno Policy %s in namespace %s: %w", template.Name, targetNamespace, err)
	}

	if err := r.deleteClusterPolicy(ctx, template.Name); err != nil {
		log.Error("Failed to delete potentially stale ClusterPolicy", "policyName", template.Name, "error", err)
	}

	policyList := &kyvernov1.PolicyList{}
	if err := r.userClient.List(ctx, policyList, ctrlruntimeclient.MatchingLabels{LabelPolicyBinding: binding.Name}); err != nil {
		return fmt.Errorf("failed to list Kyverno Policies for stale cleanup: %w", err)
	}
	for _, p := range policyList.Items {
		if p.Name == template.Name && p.Namespace == targetNamespace {
			// This is the active one, so we don't need to cleanup
			continue
		}
		if err := r.userClient.Delete(ctx, &p); ctrlruntimeclient.IgnoreNotFound(err) != nil {
			log.Error("Failed to delete stale Kyverno Policy", "policyName", p.Name, "namespace", p.Namespace, "error", err)
		}
	}

	return nil
}

// reconcileClusterPolicy reconciles a cluster-wide Kyverno Policy resource ClusterPolicy.
func (r *reconciler) reconcileClusterPolicy(ctx context.Context, log *zap.SugaredLogger, template *kubermaticv1.PolicyTemplate, binding *kubermaticv1.PolicyBinding) error {
	clusterPolicyFactories := []kkpreconciling.NamedKyvernoClusterPolicyReconcilerFactory{
		r.kyvernoClusterPolicyFactory(template, binding),
	}
	if err := kkpreconciling.ReconcileKyvernoClusterPolicys(ctx, clusterPolicyFactories, "", r.userClient); err != nil {
		return fmt.Errorf("failed to reconcile Kyverno ClusterPolicy %s: %w", template.Name, err)
	}

	policyList := &kyvernov1.PolicyList{}
	if err := r.userClient.List(ctx, policyList, ctrlruntimeclient.MatchingLabels{LabelPolicyBinding: binding.Name}); err != nil {
		return fmt.Errorf("failed to list Kyverno Policies for stale cleanup: %w", err)
	}
	for _, p := range policyList.Items {
		if err := r.userClient.Delete(ctx, &p); ctrlruntimeclient.IgnoreNotFound(err) != nil {
			log.Error("Failed to delete stale Kyverno Policy", "policyName", p.Name, "namespace", p.Namespace, "error", err)
		}
	}
	return nil
}

// namespaceReconcilerFactory creates a factory for reconciling a Namespace.
func (r *reconciler) namespaceReconcilerFactory(log *zap.SugaredLogger, nsName string) reconciling.NamedNamespaceReconcilerFactory {
	return func() (string, reconciling.NamespaceReconciler) {
		return nsName, func(ns *corev1.Namespace) (*corev1.Namespace, error) {
			return ns, nil
		}
	}
}

// kyvernoClusterPolicyFactory creates a factory for reconciling a Kyverno ClusterPolicy.
func (r *reconciler) kyvernoClusterPolicyFactory(template *kubermaticv1.PolicyTemplate, binding *kubermaticv1.PolicyBinding) kkpreconciling.NamedKyvernoClusterPolicyReconcilerFactory {
	return func() (string, kkpreconciling.KyvernoClusterPolicyReconciler) {
		return template.Name, func(cp *kyvernov1.ClusterPolicy) (*kyvernov1.ClusterPolicy, error) {
			labels := map[string]string{
				LabelPolicyBinding:  binding.Name,
				LabelPolicyTemplate: template.Name,
			}
			kuberneteshelper.EnsureLabels(cp, labels)

			annotations := map[string]string{
				AnnotationTitle:       template.Spec.Title,
				AnnotationDescription: template.Spec.Description,
			}
			if template.Spec.Category != "" {
				annotations[AnnotationCategory] = template.Spec.Category
			}
			if template.Spec.Severity != "" {
				annotations[AnnotationSeverity] = template.Spec.Severity
			}
			for k, v := range template.Annotations {
				annotations[k] = v
			}
			kuberneteshelper.EnsureAnnotations(cp, annotations)

			var spec kyvernov1.Spec
			if err := json.Unmarshal(template.Spec.PolicySpec.Raw, &spec); err != nil {
				return nil, fmt.Errorf("failed to unmarshal policySpec for ClusterPolicy %s: %w", template.Name, err)
			}
			cp.Spec = spec
			return cp, nil
		}
	}
}

// kyvernoPolicyFactory creates a factory for reconciling a Kyverno Policy (namespaced).
func (r *reconciler) kyvernoPolicyFactory(template *kubermaticv1.PolicyTemplate, binding *kubermaticv1.PolicyBinding) kkpreconciling.NamedKyvernoPolicyReconcilerFactory {
	return func() (string, kkpreconciling.KyvernoPolicyReconciler) {
		return template.Name, func(p *kyvernov1.Policy) (*kyvernov1.Policy, error) {
			labels := map[string]string{
				LabelPolicyBinding:  binding.Name,
				LabelPolicyTemplate: template.Name,
			}
			kuberneteshelper.EnsureLabels(p, labels)

			annotations := map[string]string{
				AnnotationTitle:       template.Spec.Title,
				AnnotationDescription: template.Spec.Description,
			}
			if template.Spec.Category != "" {
				annotations[AnnotationCategory] = template.Spec.Category
			}
			if template.Spec.Severity != "" {
				annotations[AnnotationSeverity] = template.Spec.Severity
			}
			for k, v := range template.Annotations {
				annotations[k] = v
			}
			kuberneteshelper.EnsureAnnotations(p, annotations)

			var spec kyvernov1.Spec
			if err := json.Unmarshal(template.Spec.PolicySpec.Raw, &spec); err != nil {
				return nil, fmt.Errorf("failed to unmarshal policySpec for Policy %s: %w", template.Name, err)
			}
			p.Spec = spec
			return p, nil
		}
	}
}

// mapPolicyBindingToRequest maps a PolicyBinding to a reconcile.Request.
func mapPolicyBindingToRequest(namespace string) func(ctx context.Context, b *kubermaticv1.PolicyBinding) []reconcile.Request {
	return func(ctx context.Context, b *kubermaticv1.PolicyBinding) []reconcile.Request {
		if b.Namespace != namespace {
			return nil
		}
		return []reconcile.Request{{NamespacedName: ctrlruntimeclient.ObjectKey{Namespace: namespace, Name: b.Name}}}
	}
}

// mapPolicyTemplateToRequest maps a PolicyTemplate to reconcile.Request for related PolicyBindings.
func mapPolicyTemplateToRequest(seedClient ctrlruntimeclient.Client, policyBindingNamespace string, log *zap.SugaredLogger) func(ctx context.Context, tpl *kubermaticv1.PolicyTemplate) []reconcile.Request {
	return func(ctx context.Context, tpl *kubermaticv1.PolicyTemplate) []reconcile.Request {
		var reqs []reconcile.Request
		bindings := &kubermaticv1.PolicyBindingList{}
		if err := seedClient.List(ctx, bindings, ctrlruntimeclient.InNamespace(policyBindingNamespace)); err != nil {
			log.Error("Failed to list PolicyBindings to map PolicyTemplate change", "template", tpl.Name, "error", err)
			return nil
		}
		for _, b := range bindings.Items {
			if b.Spec.PolicyTemplateRef.Name == tpl.Name {
				reqs = append(reqs, reconcile.Request{NamespacedName: types.NamespacedName{Namespace: b.Namespace, Name: b.Name}})
			}
		}
		return reqs
	}
}

// mapClusterPolicyToRequest maps a ClusterPolicy to a reconcile.Request based on label.
func mapClusterPolicyToRequest(namespace string) func(ctx context.Context, cp *kyvernov1.ClusterPolicy) []reconcile.Request {
	return func(ctx context.Context, cp *kyvernov1.ClusterPolicy) []reconcile.Request {
		name, ok := cp.Labels[LabelPolicyBinding]
		if !ok {
			return nil
		}
		return []reconcile.Request{{NamespacedName: ctrlruntimeclient.ObjectKey{Namespace: namespace, Name: name}}}
	}
}

// mapPolicyToRequest maps a Policy to a reconcile.Request based on label.
func mapPolicyToRequest(namespace string) func(ctx context.Context, p *kyvernov1.Policy) []reconcile.Request {
	return func(ctx context.Context, p *kyvernov1.Policy) []reconcile.Request {
		name, ok := p.Labels[LabelPolicyBinding]
		if !ok {
			return nil
		}
		return []reconcile.Request{{NamespacedName: ctrlruntimeclient.ObjectKey{Namespace: namespace, Name: name}}}
	}
}

// handlePolicyBindingCleanup handles the cleanup of a PolicyBinding and its resources.
func (r *reconciler) handlePolicyBindingCleanup(ctx context.Context, binding *kubermaticv1.PolicyBinding) error {
	if err := r.deleteKyvernoResourcesForBinding(ctx, binding); err != nil && binding.DeletionTimestamp.IsZero() {
		binding.SetCondition(kubermaticv1.PolicyBindingConditionKyvernoPolicyApplied, metav1.ConditionFalse, kubermaticv1.PolicyBindingReasonApplyFailed, err.Error())
		binding.SetCondition(kubermaticv1.PolicyBindingConditionReady, metav1.ConditionFalse, kubermaticv1.PolicyBindingReasonApplyFailed, "Failed to cleanup Kyverno resources")
		binding.SetStatusFields(nil, false)
		return err
	}

	// Set status to reflect that the policy is no longer active
	if !binding.DeletionTimestamp.IsZero() {
		binding.SetStatusFields(nil, false)
		binding.SetCondition(kubermaticv1.PolicyBindingConditionKyvernoPolicyApplied, metav1.ConditionFalse, kubermaticv1.PolicyBindingReasonDeleting, "Kyverno resources have been deleted")
		binding.SetCondition(kubermaticv1.PolicyBindingConditionReady, metav1.ConditionFalse, kubermaticv1.PolicyBindingReasonDeleting, "PolicyBinding is being deleted")
	}

	if kuberneteshelper.HasFinalizer(binding, cleanupFinalizer) {
		savedStatus := binding.Status.DeepCopy()

		if err := kuberneteshelper.TryRemoveFinalizer(ctx, r.seedClient, binding, cleanupFinalizer); err != nil {
			return fmt.Errorf("failed to remove finalizer: %w", err)
		}

		// Restore the status after TryRemoveFinalizer so the defer can persist it.
		binding.Status = *savedStatus
	}

	return nil
}

// deleteKyvernoResourcesForBinding attempts to delete all Kyverno resources associated with the binding.
func (r *reconciler) deleteKyvernoResourcesForBinding(ctx context.Context, binding *kubermaticv1.PolicyBinding) error {
	policyRefName := binding.Spec.PolicyTemplateRef.Name

	if err := r.deleteClusterPolicy(ctx, policyRefName); err != nil {
		return fmt.Errorf("failed to delete ClusterPolicy %s: %w", policyRefName, err)
	}

	if binding.Spec.KyvernoPolicyNamespace != nil && binding.Spec.KyvernoPolicyNamespace.Name != "" {
		if err := r.deleteKyvernoPolicy(ctx, policyRefName, binding.Spec.KyvernoPolicyNamespace.Name); err != nil {
			return fmt.Errorf("failed to delete Kyverno Policy %s/%s: %w", binding.Spec.KyvernoPolicyNamespace.Name, policyRefName, err)
		}
	}

	return nil
}

// deleteClusterPolicy removes the ClusterPolicy with the given name.
func (r *reconciler) deleteClusterPolicy(ctx context.Context, policyName string) error {
	if policyName == "" {
		return nil
	}
	cp := &kyvernov1.ClusterPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name: policyName,
		},
	}
	if err := r.userClient.Delete(ctx, cp); ctrlruntimeclient.IgnoreNotFound(err) != nil {
		return fmt.Errorf("failed to delete ClusterPolicy %s: %w", policyName, err)
	}

	return nil
}

// deleteKyvernoPolicy removes the namespaced Kyverno Policy.
func (r *reconciler) deleteKyvernoPolicy(ctx context.Context, policyName, policyNamespace string) error {
	if policyName == "" || policyNamespace == "" {
		return nil
	}
	p := &kyvernov1.Policy{
		ObjectMeta: metav1.ObjectMeta{
			Name:      policyName,
			Namespace: policyNamespace,
		},
	}
	if err := r.userClient.Delete(ctx, p); ctrlruntimeclient.IgnoreNotFound(err) != nil {
		return fmt.Errorf("failed to delete Policy %s/%s: %w", policyNamespace, policyName, err)
	}
	return nil
}

// updateStatus updates the PolicyBinding status.
func (r *reconciler) updateStatus(ctx context.Context, oldBinding, binding *kubermaticv1.PolicyBinding) error {
	binding.Status.ObservedGeneration = binding.Generation

	if reflect.DeepEqual(oldBinding.Status, binding.Status) {
		return nil
	}

	if err := r.seedClient.Status().Patch(ctx, binding, ctrlruntimeclient.MergeFrom(oldBinding)); err != nil {
		return fmt.Errorf("failed to update PolicyBinding status: %w", err)
	}

	return nil
}
